package se.perp.c4.control;

import java.awt.Container;
import java.awt.Point;
import java.awt.Rectangle;
import java.awt.image.BufferedImage;
import java.io.IOException;
import java.text.SimpleDateFormat;
import java.util.ArrayList;
import java.util.Date;
import java.util.List;
import java.util.Map;
import java.util.Timer;
import java.util.TimerTask;
import java.util.logging.FileHandler;
import java.util.logging.Handler;
import java.util.logging.Level;
import java.util.logging.Logger;
import java.util.logging.SimpleFormatter;
import javax.swing.Icon;
import javax.swing.JOptionPane;
import javax.swing.SwingWorker;
import se.perp.c4.control.iface.*;
import se.perp.c4.model.*;
import se.perp.c4.model.Actor.EActor;
import se.perp.c4.model.Actor.Side;
import se.perp.c4.model.Battler.Result;
import se.perp.c4.model.Card.ECard;
import se.perp.c4.model.Game.ShelobRule;
import se.perp.c4.model.Territory.ETerritory;
import se.perp.c4.net.IConnection;
import se.perp.c4.net.Message;
import se.perp.c4.ui.*;
import static se.perp.c4.model.Actor.Side.*;
import static se.perp.c4.model.Actor.EActor.*;
import static se.perp.c4.model.Territory.ETerritory.*;

public class GameController implements
        IConnectionListener,
        IPlacementCompleteListener,
        IMessageReceiver,
        IMoveListener,
        ITerritorySelectionListener,
        IGameController,
        IShadowfaxListener,
        IUIStateListener,
        IReportCreator,
        IMessenger,
        IAnimationListener {

    private MainWindow mainWindow;
    private MainMenu mainMenu;
    private ActorViewer actorViewer;
    private String playerName;
    private String opponentName = "?";
    private Container savedMainPanel;
    private PieceCollection pieceColl;
    private ActorCollection actorColl;
    private TerritoryCollection terrColl;
    private Map<EActor, ETerritory> myPlacement;
    private SpecialCard one,  two;
    private SpecialCard gandalfWhite = null,  recallToMordor = null,  palantir = null;
    private SpecialCardMiniature miniOne = null,  miniTwo = null;
    private SpecialCardMiniature gandalfWhiteMini = null,  recallToMordorMini = null,  palantirMini = null,  shadowfaxMini = null;
    private ECard myCard = null,  opponentCard = null;
    private Actor attacker = null,  defender = null;
    private Battler battler;
    /** The TCP connection to the opponent. It may also be against a bot. */
    private IConnection conn = null;
    private boolean shadowfaxPlayed = false;
    private Game game = null;
    private Deck myDeck = null;
    private Deck opponentDeck = null;
    /** Attribute that concerns the feedback the UI gives the user. */
    private boolean playSounds,  showPopups,  showCenteredMsg;
    private Logger logger;
    /** The remaining enemies report window instance. */
    private EnemyReport enemyReport = null;
    private boolean iPlayedMagic = false;
    private boolean opponentPlayedMagic = false;
    /** Special flag that we set to false when we recognize that Shadowfax has been played,
     * to delay piece thawing by one move.
     */
    private boolean thawPiecesAfterAnimation = true;
    private boolean darkHasPlayedRecall = false;
    /** This should only be used as temporary storage. Transfer to game.shelobRule ASAP,
     * (i.e. in gotConnection).
     */
    private ShelobRule shelobRule;
    private Animator animator = new Animator();
    /** For both retreat cases, which is the region we both retreat from? */
    private ETerritory retreatingFrom = null;
    private boolean dontPlayNextCling = false;

    public void animationFinished() {

        if (thawPiecesAfterAnimation) {
            thawMySide();
        } else {
            thawPiecesAfterAnimation = true;
        }
    }

    void setShelobRule(ShelobRule shelobRule) {
        this.shelobRule = shelobRule;
    }

    private void freezeMySide() {
        pieceColl.setSideMovable(game.mySide, false);
        mainWindow.setSpecialCardsResponsive(false);
    }

    private void handleMsg_name(Message m) {
        // Only the server will get here.

        opponentName = (String) m.getPayload();
        if (opponentName == null) {
            logger.severe("Got null as opponent name!");
            return;
        }
        logger.info("Got opponent name: " + opponentName);
        setupGame();
    }

    private boolean handleShelobVictoryReturnResult(ShelobVictoryReturnResult svrr, Territory defenderHome) {

        switch (svrr) {
            case SHELOB_RETURNS:
                if (playSounds) {
                    Sounds.play(Sounds.Which.RECALL);
                    dontPlayNextCling = true;
                }

                Move move = new Move(SHELOB, NOBODY, defender.where, GONDOR);
                mainWindow.setMessage(DARK, "Shelob returns to Gondor.");
                mainWindow.hidePortraits();
                thawPiecesAfterAnimation = false;
                doOpponentMoveAndAnimate(move);
                nextPlayer();
                break;
            case SHELOB_STAYS:
                battleCheckContinued(attacker, defenderHome);
                break;
            case SHELOB_DIES:
                mainWindow.setMessage(DARK, "Shelob can't return and dies!");
                mainWindow.hidePortraits();
                Actor shelob = actorColl.get(SHELOB);
                Side winner = kill(shelob);
                if (winner != null) {
                    triggerVictory(winner);
                } else {
                    nextPlayer();
                }
                break;
            case SHELOB_FIGHTS: {
                if (playSounds) {
                    Sounds.play(Sounds.Which.RECALL);
                    dontPlayNextCling = true;
                }
                mainWindow.setMessage(DARK, "Shelob returns and fights!");

                final Territory gondor = terrColl.get(GONDOR);

                int oppCount = gondor.getOccupantCount();
                int oppIx = (int) (Math.random() * oppCount);
                final Actor def = gondor.getOccupants().get(oppIx);
                defender = def;

                ETerritory shelobWhere = actorColl.get(SHELOB).where;
                Move returnMove = new Move(SHELOB, defender.who, shelobWhere, GONDOR);
                animateAttack(returnMove);
                TimerTask tt = new TimerTask() {

                    @Override
                    public void run() {
                        battleContinued(attacker, def, gondor);
                    }
                };
                Timer timer = new Timer();
                timer.schedule(tt, Animator.TIME);
                break;
            }
            default:
                return false;
        }
        return true;
    }

    private void setupGame() {
        mainWindow.setOpponentName(opponentName);
        mainWindow.setNamedTitleBar();
        mainWindow.repaint();
        String shelobRuleStr = "The game is played with the <b>\"" + Game.shelobRuleString(game.shelobRule) + "\"</b> rule:<br><br><i>" +
                Game.shelobCutRuleDescr(game.shelobRule) + "</i><br><br>";
        String dlgMsg = "<html><body>You are about to face <b>" + opponentName + "</b>.<br><br>" +
                shelobRuleStr.replaceAll("\\n", "<br>") +
                "You start as the <b>" + (game.mySide == DARK ? "dark" : "light") + "</b> player." +
                "</body></html>";
        String caption = playerName + " vs. " + opponentName;
        Icon icon = randomIcon(game.mySide);
        MessageBox.ok(mainWindow, dlgMsg, caption, icon, true);

        setupPlacementMode(game.mySide);
    }

    private void tearDown() {
        if (savedMainPanel != null) {
            // Got disconnected in placement mode, restore main window.
            mainWindow.setContainer(savedMainPanel);
            savedMainPanel = null;
        }

        game = null;
        Status.gameIsOn = false;

        if (conn != null) {
            conn.close();
            conn = null;
        }

        mainMenu.setGameAsClientEnabled(true);
        mainMenu.setGameAsServerEnabled(true);
        mainMenu.setGameGetReportEnabled(false);
        mainWindow.setReportButtonEnabled(false);

        actorColl.setAllAsLiving();
        actorColl.setAllAsUnplaced();
        pieceColl.setAllVisible(false);
        pieceColl.unmarkAll();
        terrColl.emptyAll();

        mainWindow.hideMessage(DARK);
        mainWindow.hideMessage(LIGHT);
        mainWindow.hidePortraits();
        mainWindow.setMyDeck(null);
        mainWindow.setOpponentDeck(null);
        mainWindow.setCenterMessageVisible(false);
        actorViewer.setVisible(false);

        if (one != null) {
            one.setVisible(false);
            two.setVisible(false);
        }
        if (miniOne != null) {
            miniOne.setVisible(false);
            miniTwo.setVisible(false);
        }
        mainWindow.setNormalTitleBar();

        if (enemyReport != null) {
            enemyReport.dispose();
            enemyReport = null;
        }
    }

    private void thawMySide() {
        pieceColl.setSideMovable(game.mySide, true);
        mainWindow.setSpecialCardsResponsive(true);
    }

    public void appStateChange_useHighlight(boolean selected) {
        mainWindow.setUseMovementAssistHighlight(selected);
    }

    public void connectionIsDead() {
        if (Status.gameIsOn) {
            tearDown();
            JOptionPane.showMessageDialog(mainWindow, "The connection to your opponent has been lost.\n" +
                    "The game cannot continue. Sorry.", "Oh noes!", JOptionPane.ERROR_MESSAGE);
        } else {
            mainMenu.setGameAsClientEnabled(true);
            mainMenu.setGameAsServerEnabled(true);
        }
    }

    /**
     * WaitState denotes the different states we have when waiting for a
     * message from our opponent.
     */
    public enum WaitState {

        /** Waiting for the opponent's name. */
        NAME,
        /** Waiting for opponent's initial placement. */
        PLACEMENT,
        /** Waiting for opponent to make a move. */
        MOVE,
        /** Waiting for opponent Saruman to make up his mind. */
        SARUMAN_DECISION,
        /** Waiting for opponent Frodo to make up his mind. */
        FRODO_DECISION,
        /** Waiting for opponent Sam to make up his mind. */
        SAM_DECISION,
        /** Waiting for opponent Pippin to make up his mind. */
        PIPPIN_DECISION,
        /** Waiting for a card. */
        CARD,
        /** I am Gandalf and I am waiting for my opponent's card. */
        I_GANDALF_WAITING_FOR_CARD,
        /** I am light player in a magic vs magic situation. I wait. */
        MAGIC_MAGIC_CARD_WAITING,
        /** Opponent attacked, wants to retreat, and I'm waiting for the region. */
        ATTACKER_RETREAT_MOVE,
        /** Opponent defended, wants to retreat, and I'm waiting for the region. */
        DEFENDER_RETREAT_MOVE,
        /** We have a situation where both retreat, and I'm waiting for the region. */
        SIMULTANEOUS_RETREAT_MOVE
    }
    private WaitState waitState = null;

    /**
     * Enum describing the various outcomes we can get from the
     * battleFrodoSamSpecialCase routine.
     */
    private enum SamCaseResult {

        /** Frodo and Sam were not in the same territory. */
        NO_RESULT,
        /** Frodo was the one attacked, but Sam jumps in.*/
        SAM_COVERS,
        /** Sam was the one attacked, but he's in the same territory as Frodo. */
        SAM_IS_ATTACKED_WITH_FRODO,
        /** It is the opponent who's Sam. We have fired up a MessageWaiter and should
         * wait for the opponent's SAM_DECISION. */
        WAITING_FOR_DECISION
    }

    /**
     * Enum describing the various outcomes we can get from the
     * battleFrodoSpecialCase routine.
     */
    private enum FrodoCaseResult {

        /** Frodo wasn't there, can't or chooses not to run away. */
        NO_RESULT,
        /** Frodo autoretreats sideways. */
        FRODO_FLEES,
        /** It is the opponent who is Frodo. We have fired up a MessageWaiter and
         * should wait for the opponent's FRODO_DECISION. */
        WAITING_FOR_DECISION
    }

    /**
     * Enum describing the outcomes from the checkShelobVictoryReturn routine.
     */
    private enum ShelobVictoryReturnResult {

        /** Shelob wasn't involved. */
        NO_RESULT,
        /** Shelob returns to Gondor. */
        SHELOB_RETURNS,
        /** Shelob dies (since she can't return). */
        SHELOB_DIES,
        /** Shelob returns and fights light characters in Gondor. */
        SHELOB_FIGHTS,
        /** Shelob stays where she is. */
        SHELOB_STAYS
    }

    public GameController(
            MainWindow mw,
            PieceCollection pc,
            ActorCollection ac,
            TerritoryCollection tc,
            ActorViewer av,
            String playerName) {
        this.playerName = playerName;
        mainWindow = mw;
        mainWindow.setReportCreator(this);
        mainWindow.setPlayerName(playerName);
        mainMenu = (MainMenu) mw.getJMenuBar();
        mainMenu.setUIStateListener(this);
        mainMenu.setReportCreator(this);
        playSounds = mainMenu.getPlaySounds();
        showPopups = mainMenu.getShowPopups();
        mainWindow.setUseMovementAssistHighlight(
                mainMenu.getUseHighlight());
        showCenteredMsg = mainMenu.getShowCenteredMsg();
        pieceColl = pc;
        actorColl = ac;
        terrColl = tc;
        actorViewer = av;
        animator.setup(ac, tc, pc);
    }

    /**
     * This executes the Balrog instakill procedure, with dialog and victory check
     * and whatnot.
     * @param attacker
     */
    private void balrogInstakillRoutine(final Actor attacker) {
        final BufferedImage attackerPortrait = Portraits.getLabeledNumberedPortrait(
                attacker.who, attacker.name, attacker.attackValue);
        final GamePiece balrog = pieceColl.get(BALROG);
        final BufferedImage balrogPortrait = Portraits.getLabeledNumberedPortrait(
                BALROG, "Balrog (in Moria)", actorColl.get(BALROG).attackValue);
        balrog.setKnown(true);

        // If I attacked and balrog killed me, my piece can be removed immediately.
        // If I am dark, I want to see the animation play out first, so I wait a while.
        int waitTime = 100;
        if (game.mySide == DARK) {
            waitTime = Animator.TIME;
        }

        TimerTask tt = new TimerTask() {

            @Override
            public void run() {
                new BattleResultDialog(
                        mainWindow,
                        null,
                        null,
                        attackerPortrait,
                        balrogPortrait,
                        attacker.name + " " + attacker.isAre() + " instantly defeated!",
                        BattleResultDialog.DEFAULT_TIMEOUT);

                Territory attackerHome = terrColl.get(attacker.where);
                Side winner = kill(attacker);
                if (winner == null) {
                    PieceOrderer.orderIn(terrColl, pieceColl, attackerHome);
                    nextPlayer();
                } else {
                    triggerVictory(winner);
                }
            }
        };
        Timer t = new Timer();
        t.schedule(tt, waitTime);

    }

    /**
     * With this, we check if a move causes a Balrog
     * instakill, and if it does, we also fire the instakill routine.
     * @param move
     */
    private boolean checkBalrogInstakill(Move move) {

        Actor balrog = actorColl.get(BALROG);
        if (balrog.where != MORIA) {
            return false;
        }

        if (!(move.from == EREGION && move.to == FANGORN)) {
            return false;
        }

        balrogInstakillRoutine(actorColl.get(move.attacker));
        return true;
    }

    private ShelobVictoryReturnResult checkShelobVictoryReturn(Actor actor) {
        Territory gondor = terrColl.get(GONDOR);

        if (actor.who != SHELOB || actor.where == GONDOR) {
            return ShelobVictoryReturnResult.NO_RESULT;
        }

        switch (game.shelobRule) {
            case SHELOB_DIES: {
                if (gondor.isFull() || gondor.isOccupiedByLight()) {
                    return ShelobVictoryReturnResult.SHELOB_DIES;
                } else {
                    return ShelobVictoryReturnResult.SHELOB_RETURNS;
                }
            }

            case SHELOB_FIGHTS_OR_DIES: {
                if (gondor.isFull() && gondor.isOccupiedByDark()) {
                    return ShelobVictoryReturnResult.SHELOB_DIES;
                } else if (gondor.isOccupiedByLight()) {

                    return ShelobVictoryReturnResult.SHELOB_FIGHTS;
                } else {
                    return ShelobVictoryReturnResult.SHELOB_RETURNS;
                }
            }

            case SHELOB_STAYS: {
                if (!gondor.isFull() && !gondor.isOccupiedByLight()) {
                    return ShelobVictoryReturnResult.SHELOB_RETURNS;
                } else {
                    return ShelobVictoryReturnResult.SHELOB_STAYS;
                }
            }

        }

        return ShelobVictoryReturnResult.NO_RESULT;
    }

    private void setupLogger() {
        logger = Logger.getLogger(GameController.class.getName());
        if (Main.IS_TEST_VERSION) {
            logger.setLevel(Level.ALL);
        } else {
            logger.setLevel(Level.SEVERE);
        }
        if (Main.IS_TEST_VERSION) {
            try {
                SimpleDateFormat sdf = new SimpleDateFormat("yyyy.MM.dd_HH.mm.ss");
                String fileName = "Conf_" + sdf.format(new Date()) + (conn.isServer() ? "s" : "c") + ".log";
                Handler logHandler = new FileHandler(fileName);
                logHandler.setFormatter(new SimpleFormatter());
                logger.addHandler(logHandler);
            } catch (IOException ioe) {
                // OK then. No log file will be written.
            }
        }
        logger.info("Version: " + Main.VERSION);
    }

    public void showReport() {
        if (enemyReport == null) {
            enemyReport = new EnemyReport(mainWindow, game.opponentSide, actorColl);
        } else {
            enemyReport.setVisible(!enemyReport.isVisible());
        }
    }

    private void waitForMessage() {
        logger.info("Waiting for message in wait state: " + waitState);
        new MessageWaiter(conn, this);
    }

    private SamCaseResult battleFrodoSamSpecialCase(Move move) {

        logger.info("Checking Frodo+Sam");

        if (Actor.sideFor(move.attacker) == LIGHT) {
            return SamCaseResult.NO_RESULT;
        }

        Territory t = terrColl.get(move.to);
        if (!(t.hasActor(SAM) && t.hasActor(FRODO))) {
            return SamCaseResult.NO_RESULT;
        }
        if (defender.who != FRODO && defender.who != SAM) {
            return SamCaseResult.NO_RESULT;
        }

        if (defender.who == FRODO && attacker.who != WARG) {
            // Ask Sam if he should replace Frodo.
            if (game.myTurn()) {
                // I attack, ask opponent
                setWaitState(WaitState.SAM_DECISION);
                waitForMessage();
                return SamCaseResult.WAITING_FOR_DECISION;
            } else {
                // I defend, ask me.
                int reply = MessageBox.yesNo(
                        mainWindow,
                        "Frodo is attacked by " + attacker.name + " in " + Territory.nameFor(move.to) + ".\n" +
                        "Do you want Sam to cover for him?", "Sam feat", Portraits.getIcon(SAM), false);
                boolean samDefends = (reply == JOptionPane.YES_OPTION);
                sendMessage(Message.newSamReplacesFrodo(samDefends));

                if (samDefends) {
                    return SamCaseResult.SAM_COVERS;
                } else {
                    return SamCaseResult.NO_RESULT;
                }
            }
        } else {
            // Sam was attacked Frodo by his side.
            return SamCaseResult.SAM_IS_ATTACKED_WITH_FRODO;
        }

    }

    private FrodoCaseResult battleFrodoSpecialCase(Move move) {

        logger.info("Checking Frodo");

        if (move.defender != FRODO || move.attacker == WARG) {
            return FrodoCaseResult.NO_RESULT;
        }

        assert defender.who == FRODO;

        List<ETerritory> retreatTerrs = terrColl.getValidSidewaysRetreats(defender, defender.where);
        if (retreatTerrs.size() == 0) {
            return FrodoCaseResult.NO_RESULT;
        }

        if (game.myTurn()) {
            // I am attacking Frodo.
            mainWindow.setMessage(LIGHT, "It's Frodo! Will he run away?");
            setWaitState(WaitState.FRODO_DECISION);
            waitForMessage();
            return FrodoCaseResult.WAITING_FOR_DECISION;
        } else {
            // I am Frodo.
            String whereStr = Territory.nameFor(defender.where);
            int reply = MessageBox.yesNo(
                    mainWindow,
                    "Frodo is attacked by " + attacker.name + " in " + whereStr + ".\n" +
                    "Retreat sideways?",
                    "Frodo feat",
                    Portraits.getIcon(FRODO),
                    false);
            Boolean frodoFlees = (reply == JOptionPane.YES_OPTION);

            sendMessage(Message.newFrodoInstaretreats(frodoFlees));

            if (frodoFlees) {
                new RetreatHandler(mainWindow, attacker, defender, defender.where,
                        terrColl, pieceColl, this, this,
                        RetreatMode.FRODO_INSTA_RETREAT);
                return FrodoCaseResult.FRODO_FLEES;
            } else {
                return FrodoCaseResult.NO_RESULT;
            }
        }
    }

    private void handleMsg_attackerRetreat(Message m) {
        if (m.getType() != Message.Type.RETREAT_MOVE) {
            logger.severe("I should receieve a retreat move, but instead I got: " + m + ". I will ignore this.");
            return;
        }
        Move data = (Move) m.getPayload();
        logger.info("Got atk retreat data: " + data.attacker + " flees from " + data.from + " to " + data.to);
        Move move = new Move(data.attacker, EActor.NOBODY, data.from, data.to);
        mainWindow.setMessage(game.opponentSide, "Retreat to " + Territory.nameFor(data.to) + ".");
        doOpponentMoveAndAnimate(move);
        nextPlayer();
    }

    private void handleMsg_card(Message m) {
        if (m.getType() != Message.Type.CARD) {
            logger.severe("I should receieve a card, but instead I got: " + m + ". I will ignore this.");
            return;
        }
        logger.info("Received card message: " + (ECard) m.getPayload());
        opponentCard = (ECard) m.getPayload();
        opponentPlayedMagic = (opponentCard == ECard.MAGIC);
        String cardName = opponentDeck.getCard(opponentCard).name;
        opponentDeck.discard(opponentCard);
        if (!opponentPlayedMagic) {
            opponentDeck.resetIfNeeded();
        }
        mainWindow.updateInfoPanel();

        mainWindow.setMessage(game.opponentSide,
                "Opponent played " + cardName + ".");


        // If troll is involved, disregard Magic cards.
        Actor me = (game.myTurn() ? attacker : defender);
        Actor opp = (game.myTurn() ? defender : attacker);
        if (me.who == CAVETROLL) {
            iPlayedMagic = false;
        }
        if (opp.who == CAVETROLL) {
            opponentPlayedMagic = false;
        }

        // Opponent plays magic, I play Eye of Sauron. We go straight to battle.
        // Fix 25 mar 2009: Troll should be an exception!
        if (opponentPlayedMagic && myCard == ECard.EYE_OF_SAURON &&
                me.who != CAVETROLL) {
            showEyeVsMagicDialog();
            battlePhaseTwo(attacker, defender, null, null);
            opponentPlayedMagic = false;
            return;
        }

        // I play magic, my opponent plays Eye of Sauron. We go straight to battle.
        // Fix 25 mar 2009: Troll should be an exception!
        if (iPlayedMagic && opponentCard == ECard.EYE_OF_SAURON &&
                opp.who != CAVETROLL) {
            showEyeVsMagicDialog();
            battlePhaseTwo(attacker, defender, null, null);
            iPlayedMagic = false;
            return;
        }


        // My opponent plays magic. I wait for the "real" card.
        if (opponentPlayedMagic && !iPlayedMagic) {
            logger.info("Recognized that opponent played MAGIC and I didn't.");
            waitForMessage();
            return;
        }


        // I play magic, my opponent does not. I choose a new card.
        if (iPlayedMagic && !opponentPlayedMagic) {
            logger.info("Recognized that I played MAGIC and my opponent didn't.");

            Card card = opponentDeck.getCard(opponentCard);
            final CardDisplayDialog dlg = new CardDisplayDialog(mainWindow, "Magic", card);
            SwingWorker<Void, Void> w = new SwingWorker<Void, Void>() {

                @Override
                protected Void doInBackground() throws Exception {
                    pickCard(dlg, true);
                    ECard atkCard, defCard;
                    if (game.myTurn()) {
                        atkCard = myCard;
                        defCard = opponentCard;
                    } else {
                        atkCard = opponentCard;
                        defCard = myCard;
                    }
                    sendCard(myCard);
                    battlePhaseTwo(attacker, defender, atkCard, defCard);
                    iPlayedMagic = false;
                    return null;
                }
            };
            w.execute();
            return;
        }

        // Magic vs magic
        if (iPlayedMagic && opponentPlayedMagic) {
            logger.info("Recognized that both have played MAGIC.");

            if (game.mySide == DARK) {
                // I choose first.

                pickSendAndWaitForCard(new CardDisplayDialog(mainWindow, "Magic", opponentDeck.getCard(opponentCard)));
                iPlayedMagic = false;
                return;
            } else {
                // I wait for dark's card first.
                setWaitState(WaitState.MAGIC_MAGIC_CARD_WAITING);
                waitForMessage();
                return;
            }
        }

        logger.info("No magic involved.");
        battlePhaseTwo(attacker, defender, attackerCard(), defenderCard());
    }

    private void handleMsg_defenderRetreat(Message m) {
        if (m.getType() != Message.Type.RETREAT_MOVE) {
            logger.severe("I should receieve a retreat move, but instead I got: " + m + ". I will ignore this.");
            return;
        }
        final Move data = (Move) m.getPayload();
        logger.info("Got def retreat data: " + data.attacker + " flees from " + data.from + " to " + data.to);
        mainWindow.setMessage(game.opponentSide, "Retreat to " + Territory.nameFor(data.to) + ".");
        // Timing is crucial here. We can't doOpponentMoveAndAnimate, since
        // that would leave the character unmoved when battleCheckContinued runs.
        // So the strategy is to initialize the animation first, then move ourselves
        // and do the check.

        pieceColl.get(data.attacker).unmark();

        animator.queueAnimation(data, null, false, true);
        animator.start();

        terrColl.moveActor(actorColl.get(data.attacker), data.to);
        final Territory t = terrColl.get(data.from);
        battleCheckContinued(attacker, t);
    }

    private void handleMsg_simultaneousRetreat(Message m) {
        final Move move = (Move) m.getPayload();
        logger.info("Got def retreat data: " + move.attacker + " flees from " + move.from + " to " + move.to);

        thawPiecesAfterAnimation = false;

        if (game.mySide == DARK) {
            animator.queueAnimation(move, this, true, true);
            animator.start();
            nextPlayer();
        } else {
            animator.queueAnimation(move, null, true, true);
            animator.start();
            final IMessenger im = this;
            final IGameController igc = this;
            final SwingWorker<Void, Void> w = new SwingWorker<Void, Void>() {

                @Override
                protected Void doInBackground() throws Exception {
                    final Actor retreater = (game.myTurn() ? attacker : defender);
                    new RetreatHandler(mainWindow, attacker, retreater, retreatingFrom,
                            terrColl, pieceColl, im, igc, RetreatMode.JUST_NOTIFY_WHEN_DONE);
                    return null;
                }
            };

            TimerTask tt = new TimerTask() {

                @Override
                public void run() {
                    w.execute();
                }
            };
            new Timer().schedule(tt, Animator.TIME + 100); // To be certain animation  completes.

        }

    }

    private void handleMsg_frodoRetreat(Message m) {
        assert m.getType() == Message.Type.FRODO_INSTARETREATS;

        final boolean frodoFlees = (Boolean) m.getPayload();

        if (frodoFlees) {
            Battler.setOrcsHaveAttacked(true);
            logger.info("Frodo flees!");
            mainWindow.setMessage(LIGHT, "Frodo flees. Where to?");
            setWaitState(WaitState.DEFENDER_RETREAT_MOVE);
            waitForMessage();
        } else {
            logger.info("Frodo does NOT retreat.");

            // In case Frodo does not flee when facing orcs... well,
            // he's an idiot, and he should die. :)
            if (attacker.who == ORCS && defender.who == FRODO && Battler.orcsMayKill()) {
                battleInstakill(attacker, defender, Result.ATTACKER_WIN);
                return;
            }

            mainWindow.setMessage(LIGHT, "Frodo fights.");
            Territory t = terrColl.get(defender.where);
            assert t.hasActor(FRODO);
            if (attacker.who == SARUMAN) {
                Move move = new Move(attacker.who, FRODO, attacker.where, t.which);
                SarumanCaseResult r = battleSarumanSpecialCase(move);
                switch (r) {
                    case PLAYS_CARDS:
                        pickSendAndWaitForCard();
                        break;
                    case DOES_NOT_PLAY_CARDS:
                        battlePhaseTwo(attacker, defender, null, null);
                        break;
                    default:
                        logger.severe("Should never be here - Saruman should be the attacker, I should never have to wait.");
                        break;
                }
            } else {
                pickSendAndWaitForCard();
            }
        }
    }

    private void handleMsg_iGandalfWaitingForCard(Message m) {

        opponentCard = (ECard) m.getPayload();
        opponentDeck.discard(opponentCard);

        String cardName = opponentDeck.getCard(opponentCard).name;
        mainWindow.setMessage(DARK, "Opponent played " + cardName + ".");

        if (opponentCard == ECard.MAGIC) {
            opponentPlayedMagic = true;
        }

        final CardDisplayDialog dlg = new CardDisplayDialog(mainWindow, "Opponent plays:", opponentDeck.getCard(opponentCard));


        SwingWorker<Void, Void> w = new SwingWorker<Void, Void>() {

            @Override
            protected Void doInBackground() throws Exception {
                boolean done = true;
                iPlayedMagic = false;
                do {
                    done = true;
                    pickCard(dlg, iPlayedMagic);
                    sendCard(myCard);
                    iPlayedMagic = (myCard == ECard.MAGIC);

                    if (opponentPlayedMagic) {
                        if (iPlayedMagic) {
                            setWaitState(WaitState.MAGIC_MAGIC_CARD_WAITING);
                            waitForMessage();
                        } else {
                            setWaitState(WaitState.CARD);
                            waitForMessage();
                        }
                    } else {
                        if (!iPlayedMagic) {
                            battlePhaseTwo(attacker, defender, attackerCard(), defenderCard());
                        } else {
                            if (opponentCard == ECard.EYE_OF_SAURON) {
                                showEyeVsMagicDialog();
                                battlePhaseTwo(attacker, defender, null, null);
                            } else {
                                done = false; // loop again and pick the card.
                            }
                        }
                    }
                } while (!done);
                iPlayedMagic = false;

                return null;
            }
        };
        w.execute();

    }

    private void showEyeVsMagicDialog() {
        if (game.mySide == DARK) {
            MessageBox.ok(mainWindow,
                    "<html><body>Your <i>Eye of Sauron</i> cancels<br>your opponent's <i>Magic!</i></body></html>",
                    "A tricky situation", Images.MAGIC_VS_EYE, true);
        } else {
            MessageBox.ok(mainWindow,
                    "<html><body><i>Eye of Sauron</i> cancels<br>the effect of your <i>Magic</i>!</body></html>",
                    "A tricky situation", Images.MAGIC_VS_EYE, true);
        }
    }

    private void handleMsg_magicVsMagic(Message m) {
        // I get here when I am light and have the benefit (?) of choosing
        // card last in a magic vs magic situation.

        opponentCard = (ECard) m.getPayload();
        Card card = opponentDeck.getCard(opponentCard);

        final CardDisplayDialog dlg = new CardDisplayDialog(mainWindow, "Magic play:", card);
        final boolean letLightPick = opponentCard != ECard.EYE_OF_SAURON;
        if (opponentCard == ECard.EYE_OF_SAURON) {
            mainWindow.setMessage(DARK, "Opponent plays Eye of Sauron!");
            showEyeVsMagicDialog();
            opponentCard = null;
            myCard = null;
        }

        SwingWorker<Void, Void> w = new SwingWorker<Void, Void>() {

            @Override
            protected Void doInBackground() throws Exception {
                if (letLightPick) {
                    pickCard(dlg, true);
                    sendCard(myCard);
                }

                battlePhaseTwo(attacker, defender, attackerCard(), defenderCard());
                return null;
            }
        };
        w.execute();
    }

    private void sendCard(ECard c) {
        logger.info("Sending card: " + c);
        sendMessage(Message.newCard(c));
    }

    private ECard attackerCard() {
        return (game.myTurn() ? myCard : opponentCard);
    }

    private ECard defenderCard() {
        return (game.myTurn() ? opponentCard : myCard);
    }

    private void handleMsg_pippin(Message m) {
        boolean pippinRetreats = (Boolean) m.getPayload();
        logger.info("Got Pippin decision, retreats=" + pippinRetreats);
        if (pippinRetreats) {
            mainWindow.setMessage(LIGHT, "Pippin retreats. Where to?");
            setWaitState(WaitState.MOVE);
            waitForMessage();
        } else {
            mainWindow.setMessage(LIGHT, "Pippin takes a stand!");
            // If Pippin chooses NOT to retreat when he faces Saruman,
            // we have to take special care to this by faking a new move
            // and sending it to battleSarumanSpecialCase().
            //
            // In other cases, we pick a card and do battle as expected.
            Move move = new Move(PIPPIN, SARUMAN, actorColl.get(PIPPIN).where, actorColl.get(SARUMAN).where);
            if (battleSarumanSpecialCase(move) == SarumanCaseResult.DOES_NOT_PLAY_CARDS) {
                battlePhaseTwo(attacker, defender, attackerCard(), defenderCard());
            } else {
                pickSendAndWaitForCard();
            }
        }
    }

    @SuppressWarnings("unchecked")
    private void handleMsg_placement(Message m) {

        Map<EActor, ETerritory> oppPlacement = null;

        try {
            oppPlacement = (Map<EActor, ETerritory>) m.getPayload();
            logger.info("Got opponent placement: " + oppPlacement);
        } catch (Exception e) {
            logger.severe("Couldn't cast placement!");
            crash("Couldn't cast placement!");
        }

        logger.info("===== I play as the " + game.mySide + " side =====");

        setupBoard(oppPlacement);

        mainWindow.setCenterMessageVisible(false);

        if (game.myTurn()) {
            mainWindow.setMessage(game.mySide, "It's your turn.");
            thawMySide();
        } else {
            if (showCenteredMsg) {
                mainWindow.setCenterMessage("Waiting for opponent to move...");
                mainWindow.setCenterMessageVisible(true);
            }
        }
        mainWindow.setReportButtonEnabled(true);
        mainMenu.setGameGetReportEnabled(true);
        mainWindow.setReportButtonEnabled(true);

        setWaitState(WaitState.MOVE);
        gameLoop();
    }

    private void handleMsg_samReplacesFrodo(Message m) {
        boolean samReplaces = (Boolean) m.getPayload();

        if (samReplaces) {
            logger.info("I hear that SAM replaces FRODO.");
            mainWindow.setMessage(LIGHT, "Sam covers for Frodo!");
            defender = actorColl.get(SAM);
            battler = new Battler(attacker, defender, terrColl);
            showPortraitsForCurrentAttack();

            Result r = battler.instantResolution();
            if (r != Result.NO_RESULT) {
                battleInstakill(attacker, defender, r);
            } else if (attacker.who == SARUMAN) {
                SarumanCaseResult res = battleSarumanSpecialCase(
                        new Move(attacker.who, defender.who, ETerritory.NOWHERE, defender.where));
                switch (res) {
                    case DOES_NOT_PLAY_CARDS:
                        battlePhaseTwo(attacker, defender, null, null);
                        break;
                    case PLAYS_CARDS:
                        pickSendAndWaitForCard();
                        break;
                    default:
                        logger.warning("We should not be here. Saruman either plays or does not play cards, and he's on our side!");
                }
            } else {
                pickSendAndWaitForCard();
            }
        } else {

            if (defender.who == FRODO) {
                battleFrodoSpecialCase(new Move(attacker.who, defender.who, ETerritory.NOWHERE, defender.where));
            } else {
                logger.severe("Found that Sam covered for someone other than Frodo!");
                pickSendAndWaitForCard();
            }
        }
    }

    private void handleMsg_saruman(Message m) {
        boolean playCards = (Boolean) m.getPayload();
        assert game.mySide == LIGHT;
        if (playCards) {
            if (attacker.who == GANDALF || defender.who == GANDALF) {
                mainWindow.setMessage(DARK, "Saruman picks first...");
                setWaitState(WaitState.I_GANDALF_WAITING_FOR_CARD);
                waitForMessage();
            } else {
                mainWindow.setMessage(DARK, "Saruman wants to play cards.");
                pickSendAndWaitForCard();
            }
        } else {
            mainWindow.setMessage(DARK, "Saruman wants no cards.");
            battlePhaseTwo(attacker, defender, null, null);
        }
    }

    public void territorySelected(ETerritory et) {
        // We get here when I attack with Pippin and autoretreat.

        assert attacker.who == PIPPIN : "Should only come here with Pippin!";

        mainWindow.setContainer(savedMainPanel);

        terrColl.moveActor(attacker, et);
        PieceOrderer.orderAll(terrColl, pieceColl);

        pieceColl.get(PIPPIN).setNormalBackground(); // Hack.

        mainWindow.setContainer(savedMainPanel);
        mainWindow.repaint();

        sendMessage(Message.newPippinRetreatChoice(et));

        nextPlayer();
    }

    /**
     * This routine checks if there is to be continued battle in a certain area.
     * If true, a timer task will fire battleContinued().
     */
    public void battleCheckContinued(final Actor attacker, final Territory defenderHome) {

        assert defenderHome != null;

        Side defenderSide = (attacker.isDark() ? LIGHT : DARK);

        boolean moreToFight = defenderHome.isOccupiedBy(defenderSide);

        if (moreToFight) {
            logger.info("There are more to fight in " + defenderHome + "!");
            // Wait a while for the sake of the battle result dialog.
            // TODO: Should we instead have a listener on the dialog?

            TimerTask tt = new TimerTask() {

                @Override
                public void run() {
                    battleContinued_pickRandomDefender(attacker, defenderHome);
                }
            };
            Timer t = new Timer("ContinuedBattleTrigger");
            t.schedule(tt, 2000);
        } else {
            // This was the last enemy.
            mainWindow.hideMessage(game.whoseTurn); // TODO: Unsure if this does any good
            Territory from = terrColl.get(attacker.where);
            terrColl.moveActor(attacker, defenderHome.which);
            Territory to = terrColl.get(attacker.where);
            if (from != null) {
                PieceOrderer.orderIn(terrColl, pieceColl, from);
            }
            PieceOrderer.orderIn(terrColl, pieceColl, to);

            nextPlayer();
        }
    }

    private Side moveTriggersVictory(Move move) {
        // Did Frodo just move into MORDOR? In that case, the round is won.
        if (move.attacker == FRODO && move.to == MORDOR) {
            return LIGHT;
        }

        // Did a dark player move into SHIRE, and would there now be 3 of
        // them there? In that case, the round is won.
        if (Actor.sideFor(move.attacker) == DARK &&
                move.to == SHIRE &&
                terrColl.get(SHIRE).isOccupiedByDark() &&
                terrColl.get(SHIRE).getOccupantCount() == 3) {
            return DARK;
        }
        return null;
    }

    private void pickSendAndWaitForCard() {
        pickSendAndWaitForCard(null);
    }

    private void pickSendAndWaitForCard(final CardDisplayDialog dlg) {
        final boolean useDiscardPile = iPlayedMagic;

        SwingWorker<Void, Void> w = new SwingWorker<Void, Void>() {

            @Override
            protected Void doInBackground() throws Exception {
                pickCard(dlg, useDiscardPile);

                sendCard(myCard);

                if (useDiscardPile && opponentPlayedMagic &&
                        myCard == ECard.EYE_OF_SAURON) {
                    myCard = null;
                    showEyeVsMagicDialog();
                    battlePhaseTwo(attacker, defender, null, null);
                } else {
                    setWaitState(WaitState.CARD);
                    mainWindow.setMessage(game.opponentSide, "Waiting for opponent card...");
                    waitForMessage();
                }
                return null;
            }
        };
        w.execute();
    }

    private void pickCard(final CardDisplayDialog oppCardDlg, boolean useDiscardPile) {

        final Actor opponent = (game.myTurn() ? defender : attacker);

        Card selected = null;
        boolean okCard = false;
        boolean firstLoop = true;
        do {
            CardPickerDialog dlg = new CardPickerDialog(mainWindow, myDeck, opponentDeck, opponent, useDiscardPile);
            if (oppCardDlg != null && firstLoop) {
                dlg.setLocation(dlg.getX(), dlg.getY() + oppCardDlg.getHeight() / 2);

                oppCardDlg.setLocation(
                        dlg.getX() + (dlg.getWidth() - oppCardDlg.getWidth()) / 2,
                        dlg.getY() - oppCardDlg.getHeight() - 5);
                oppCardDlg.setVisible(true);
            }
            firstLoop = false;
            dlg.setVisible(true);

            while (dlg.isVisible()) {
                try {
                    Thread.sleep(50);
                } catch (InterruptedException ie) {
                }
            }

            selected = dlg.getSelectedCard();
            okCard = (selected.type != ECard.MAGIC || myDeck.getDiscardedCount() != 0);

            if (selected.type == ECard.MAGIC && okCard) {
                iPlayedMagic = true;
            } else {
                iPlayedMagic = false;
            }

        } while (!okCard);

        if (oppCardDlg != null) {
            oppCardDlg.dispose();
        }

        myCard = selected.type;

        if (!useDiscardPile) {
            myDeck.discard(myCard);
        }
        if (!iPlayedMagic) {
            myDeck.resetIfNeeded();
        }
        if (!opponentPlayedMagic) {
            opponentDeck.resetIfNeeded();
        }
        mainWindow.updateInfoPanel();
    }

    public void appStateChange_sound(boolean playSounds) {
        this.playSounds = playSounds;
    }

    public void gotConnection(IConnection conn) {
        this.conn = conn;

        mainWindow.setMyScore(0);
        mainWindow.setOpponentScore(0);

        conn.setConnectionListener(this);

        setupLogger();
        Status.gameIsOn = true;

        Side mySide = null;
        game = new Game();
        game.shelobRule = shelobRule;

        if (!conn.isServer()) {
            try {
                logger.info("Starting new game as CLIENT.");
                Message msg;
                msg = conn.waitForNextMessage();
                logger.info("Got msg: " + msg);
                mySide = (Side) msg.getPayload();

                msg = conn.waitForNextMessage();
                logger.info("Got msg: " + msg);
                game.shelobRule = (Game.ShelobRule) msg.getPayload();

                msg = conn.waitForNextMessage();
                logger.info("Got msg: " + msg);
                opponentName = (String) msg.getPayload();

                sendMessage(Message.newName(playerName));

                game.mySide = mySide;
                game.opponentSide = (mySide == LIGHT ? DARK : LIGHT);

                mainWindow.setHomeSide(mySide);
                PieceOrderer.setHomeSide(mySide);

                setupGame(); // Server will go there after the client's name is received.
            } catch (Exception e) {
//                conn.close();
                crash("Network communication couldn't synchronize.\n" +
                        "- Are you using the same version of the program as the server?\n\n" +
                        "Please restart the application and try again!");
            }
        } else {
            logger.info("Starting new game as SERVER.");
            mySide = game.mySide;

            mainWindow.setHomeSide(mySide);
            PieceOrderer.setHomeSide(mySide);

            sendMessage(Message.newSide(game.opponentSide));
            sendMessage(Message.newShelobRule(game.shelobRule));
            sendMessage(Message.newName(playerName));

            waitState = WaitState.NAME;
            waitForMessage();
        }

        setupDecks();
    }

    private void crash() {
        JOptionPane.showMessageDialog(mainWindow, "The application has gone into an unstable state and must be closed.", "Not good", JOptionPane.ERROR_MESSAGE);
        System.exit(1);
    }

    private void crash(String reason) {
        JOptionPane.showMessageDialog(mainWindow, "The application has gone into an unstable state and must be closed.\n\nThe reason was:\n" + reason, "Not good", JOptionPane.ERROR_MESSAGE);
        System.exit(1);
    }

    private void newRound(Side mySide) {

        darkHasPlayedRecall = false;
        shadowfaxPlayed = false;
        iPlayedMagic = false;
        opponentPlayedMagic = false;

        logger.info("===== NEW ROUND =====");

        game.mySide = mySide;
        game.opponentSide = (game.mySide == LIGHT ? DARK : LIGHT);
        game.whoseTurn = DARK;

        logger.info("===== I am now the " + game.mySide + " player =====");

        mainWindow.hidePortraits();
        mainWindow.hideMessage(LIGHT);
        mainWindow.hideMessage(DARK);

        PieceOrderer.setHomeSide(mySide);

        pieceColl.setSideKnown(mySide, true);
        pieceColl.unmarkAll();

        mainWindow.setReportCreator(this);

        String dlgMsg = "You play as the " + (mySide == DARK ? "dark" : "light") + " player\nin the second round.";
        Icon icon = Portraits.getIcon((mySide == DARK ? Actor.EActor.BALROG : Actor.EActor.GANDALF));

        MessageBox.ok(mainWindow, dlgMsg, "Round 2 about to begin", icon, true);

        pieceColl.setSideKnown(game.opponentSide, false);
        actorViewer.setActor(null);
        actorColl.setAllAsLiving();
        actorColl.setAllAsUnplaced();
        terrColl.emptyAll();

        setupDecks();
        setupSpecialCards();

        mainWindow.setHomeSide(mySide);

        setupPlacementMode(mySide);
    }

    public void placementComplete(
            Map<EActor, ETerritory> placement,
            Map<EActor, Point> positions) {
        myPlacement = placement;
        logger.info("Got initial placement: " + placement);

        if (savedMainPanel != null) {
            mainWindow.setContainer(savedMainPanel);
            mainWindow.repaint();
            savedMainPanel = null;
        }

        terrColl.emptyAll();
        actorColl.setAllAsUnplaced();
        pieceColl.setAllVisible(false);
        for (EActor ea : myPlacement.keySet()) {
            Actor a = actorColl.get(ea);
            Territory t = terrColl.get(myPlacement.get(ea));
            GamePiece gp = pieceColl.get(ea);
            t.addActor(a);
            gp.setLocation(positions.get(ea));
            gp.setKnown(true);
            gp.setMovable(false);
            gp.setVisible(true);
            gp.setNormalBackground();
        }

        mainWindow.addPieces(pieceColl);

        PieceOrderer.orderAll(terrColl, pieceColl);

        PieceHandler handler = new PieceHandler(mainWindow, actorViewer, actorColl, terrColl,
                pieceColl, new MoveValidator(terrColl), this);
        pieceColl.setGamePieceListener(handler);

        sendMessage(Message.newPlacement(placement));

        mainWindow.setActorViewer(actorViewer);
        actorViewer.setVisible(true);
        actorViewer.setActor(null);

        mainWindow.setCenterMessage("Waiting for initial placement...");
        mainWindow.setCenterMessageVisible(true);

        setupSpecialCards();

        setWaitState(WaitState.PLACEMENT);
        waitForMessage();
    }

    private void setupPlacementMode(Side side) {
        pieceColl.setAllVisible(false);
        pieceColl.setSideKnown(side, true);
        mainWindow.setReportButtonEnabled(false);

        if (enemyReport != null) {
            enemyReport.setOpponentSide(game.opponentSide);
        }

        Rectangle holdingBox = new Rectangle(600, actorViewer.getHeight() + 20, 200, 350);
        EActor[] actors = (side == LIGHT ? Actor.LIGHT_ACTORS : Actor.DARK_ACTORS);
        PlacementPanel placementPanel = new PlacementPanel(
                mainWindow,
                pieceColl,
                actorViewer,
                actors,
                TerritoryCollection.placementTerritories(side));
        PlacementHandler ppl = new PlacementHandler(
                placementPanel,
                side,
                holdingBox,
                actorColl,
                terrColl,
                pieceColl,
                actorViewer);
        ppl.setPlacementCompleteListener(this);

        IPlacementButtonListener ipbl = ppl;
        IGamePieceListener igpl = ppl;

        pieceColl.setGamePieceListener(igpl);
        pieceColl.setSideMovable(side, true);
        placementPanel.setPlacementButtonListener(ipbl);

        savedMainPanel = mainWindow.getContainer();
        mainWindow.setContainer(placementPanel);
        mainWindow.repaint();
    }

    private void setWaitState(WaitState s) {
        waitState = s;
        logger.info("Setting wait state: " + s);
    }

    public void serverConnectionFailed(String host, int port) {
        JOptionPane.showMessageDialog(mainWindow,
                "Could not connect to host " + host + " on port " + port + ".\n\n" +
                "- Are you connected to the host (via Internet/LAN)?\n" +
                "- Does the host exist (did you enter the right IP)?\n" +
                "- Is the host awating a connection attempt?\n" +
                "- Does the host's firewall allow incoming connections on port " + port + "?\n" +
                "- Is the port used by another program?",
                "Client creation failed",
                JOptionPane.ERROR_MESSAGE);
    }

    public void clientConnectionFailed(int port) {
        JOptionPane.showMessageDialog(mainWindow,
                "Could not setup server on port " + port + ".\n\n" +
                "- Is the port used by another program?",
                "Server creation failed",
                JOptionPane.ERROR_MESSAGE);
    }

    public void messageReceived(Message m) {
        logger.info("Got msg: " + m.getType() + ". My waitState=" + waitState);

        mainWindow.clearPalantirHighlight();

        switch (waitState) {
            case NAME:
                handleMsg_name(m);
                break;

            case PLACEMENT:
                handleMsg_placement(m);
                break;

            case MOVE:
                handleMsg_move(m);
                break;

            case SARUMAN_DECISION:
                handleMsg_saruman(m);
                break;

            case PIPPIN_DECISION:
                handleMsg_pippin(m);
                break;

            case CARD:
                handleMsg_card(m);
                break;

            case I_GANDALF_WAITING_FOR_CARD:
                handleMsg_iGandalfWaitingForCard(m);
                break;

            case ATTACKER_RETREAT_MOVE:
                handleMsg_attackerRetreat(m);
                break;

            case DEFENDER_RETREAT_MOVE:
                handleMsg_defenderRetreat(m);
                break;

            case SIMULTANEOUS_RETREAT_MOVE:
                handleMsg_simultaneousRetreat(m);
                break;

            case FRODO_DECISION:
                handleMsg_frodoRetreat(m);
                break;

            case SAM_DECISION:
                handleMsg_samReplacesFrodo(m);
                break;

            case MAGIC_MAGIC_CARD_WAITING:
                handleMsg_magicVsMagic(m);
                break;

            default:

                crash("Unhandled wait state in GameController!");
        }

    }

    private void setupBoard(Map<EActor, ETerritory> oppPlacement) {

        List<GamePiece> oppPieces = new ArrayList<GamePiece>();

        for (EActor ea : oppPlacement.keySet()) {
            Actor a = actorColl.get(ea);
            Territory t = terrColl.get(oppPlacement.get(ea));

            GamePiece gp = pieceColl.get(ea);
            t.addActor(a);
            gp.setKnown(false);
            gp.setMovable(false);
            gp.setVisible(true);
            gp.setNormalBackground();
            oppPieces.add(gp);
        }

        mainWindow.addPieces(oppPieces);

        PieceOrderer.orderAll(terrColl, pieceColl);

        pieceColl.setSideMovable(game.mySide, (game.mySide == DARK));
        pieceColl.setSideMovable(game.opponentSide, false);

        mainWindow.repaint();

    }

    private void gameLoop() {
        mainWindow.repaint();

        Side mySide = game.mySide;

        myCard = null;
        opponentCard = null;

        iPlayedMagic = false;
        opponentPlayedMagic = false;

        updateSpecialCards();

        pieceColl.setSideKnown(game.opponentSide, false);
        pieceColl.unmarkAll();

        actorColl.get(SAM).attackValue = 2;

        if (checkDarkCantMoveVictory()) {
            return;
        }


        PieceOrderer.orderAll(terrColl, pieceColl);
        if (game.myTurn()) {
            mainWindow.setCenterMessageVisible(false);
            logger.info("  == MY TURN ==");
        } else {
            logger.info("  == ENEMY TURN ==");
            // Only order after our moves. Animation routine takes care of
            // ordering after an enemy move.            
            mainWindow.hideMessage(mySide);
            setWaitState(WaitState.MOVE);
            waitForMessage();
        }

    }

    private void doOpponentMoveAndAnimate(final Move move) {
        animator.queueAnimation(move, this, true, true);
        animator.start();
    }

    private void animateAttack(final Move move) {
        GamePiece atkPiece = pieceColl.get(move.attacker);
        atkPiece.setKnown(true);
        GamePiece defPiece = pieceColl.get(move.defender);
        defPiece.mark();

        animator.queueAnimation(move, this, false, false);
        animator.start();
    }

    private void updateSpecialCards() {
        if (game.mySide == LIGHT) {
            gandalfWhite.setEnabled(!gandalfWhite.hasBeenPlayed() &&
                    terrColl.canPlayGandalfWhite());
        } else {
            recallToMordor.setEnabled(!recallToMordor.hasBeenPlayed() &&
                    terrColl.canPlayRecallToMordor());

            palantir.setEnabled(!palantir.hasBeenPlayed() &&
                    terrColl.canPlayPalantir());
        }
    }

    public void moveMade(Move move) {

        if (!shadowfaxPlayed) {
            pieceColl.setSideMovable(game.mySide, false);
        } else {
            assert game.mySide == LIGHT;
        }

        // We come here whenever home has made a move.

        Side winner = moveTriggersVictory(move);
        if (winner != null) {
            sendMessage(Message.newMove(move));
            triggerVictory(winner);
            return;
        }

        sendMessage(Message.newMove(move));

        if (checkBalrogInstakill(move)) {
            return;
        }

        nextPlayer();
    }

    public void attackMoveMade(Move move) {

        if (!shadowfaxPlayed) {
            pieceColl.setSideMovable(game.mySide, false);
        } else {
            assert game.mySide == LIGHT;
        }
        sendMessage(Message.newBattleMove(move));

        if (checkBalrogInstakill(move)) {
            return;
        }

        String defName = actorColl.get(move.defender).name;
        mainWindow.setMessage(game.mySide, "You encounter " + defName + "!");

        GamePiece defPiece = pieceColl.get(move.defender);
        defPiece.setKnown(true);

        showPortraitsForAttack(move);
        battle(move);
    }

    public void recallMoveMade(Move move) {
        // We come here whenever home has made a RECALL TO MORDOR move.
        darkHasPlayedRecall = true;
        sendMessage(Message.newRecallMove(move));
        setWaitState(WaitState.MOVE);
        nextPlayer();
    }

    private void setupSpecialCards() {
        if (game.mySide == LIGHT) {
            one = new Shadowfax(new ShadowfaxHandler(this, this));
            two = new GandalfWhite(new GandalfWhiteHandler(
                    actorColl, terrColl, pieceColl, this, this));

            gandalfWhite = two;

            palantirMini = new SpecialCardMiniature(new Palantir(null));
            recallToMordorMini = new SpecialCardMiniature(new Recall(null));
            miniOne = palantirMini;
            miniTwo = recallToMordorMini;

        } else {
            one = new Palantir(new PalantirRevealHandler(mainWindow, pieceColl, terrColl, this));
            palantir = one;
            two = new Recall(new RecallToMordorHandler(actorColl, terrColl, pieceColl, this));
            recallToMordor = two;

            gandalfWhiteMini = new SpecialCardMiniature(new GandalfWhite(null));
            shadowfaxMini = new SpecialCardMiniature(new Shadowfax(null));
            miniOne = gandalfWhiteMini;
            miniTwo = shadowfaxMini;
        }

        one.setWarningOwner(mainMenu);
        two.setWarningOwner(mainMenu);
        one.setResponsive(false);
        two.setResponsive(false);

        mainWindow.setSpecialCards(one, two);
        mainWindow.setSpecialCardMiniatures(miniOne, miniTwo);
    }

    private void handleMsg_move(Message m) {

        switch (m.getType()) {
            case MOVE: {
                mainWindow.hideMessage(game.opponentSide);
                if (!shadowfaxPlayed) {
                    mainWindow.setCenterMessageVisible(false);
                }

                Move move = (Move) m.getPayload();
                logger.info("  The move is: " + move);

                // Crucial sequence: we must animate before we move.
                // But we must also move immediately before we check for
                // triggers. This is why we both fire up the animation and
                // move manually before checking for victory triggers.
                animator.queueAnimation(move, this, false, true);
                animator.start();

                if (checkBalrogInstakill(move)) {
                    break;
                }

                Actor a = actorColl.get(move.attacker);
                terrColl.moveActor(a, move.to);
                Side winner = moveTriggersVictory(move);

                if (winner == null) {
                    nextPlayer();
                } else {
                    triggerVictory(winner);
                }

                break;

            }
            case PALANTIR_PLAYED: {
                if (playSounds) {
                    Sounds.play(Sounds.Which.PALANTIR);
                }
                ETerritory et = (ETerritory) m.getPayload();
                mainWindow.setMessage(Actor.Side.DARK,
                        "Plays Palantir in " + Territory.nameFor(et) + ".");
                mainWindow.setPalantirHighlight(et);
                setWaitState(WaitState.MOVE);
                waitForMessage();
                palantirMini.setCrossedOut(true);
                break;

            }
            case RECALL_MOVE: {
                if (playSounds) {
                    Sounds.play(Sounds.Which.RECALL);
                }
                dontPlayNextCling = true;
                darkHasPlayedRecall = true;
                Move move = (Move) m.getPayload();
                mainWindow.setMessage(Actor.Side.DARK,
                        "Recalls from " + Territory.nameFor(move.from) + ".");

                animator.queueAnimation(move, this, false, true);
                animator.start();
                terrColl.moveActor(actorColl.get(move.attacker), move.to);
                recallToMordorMini.setCrossedOut(true);
                nextPlayer();

                break;
            }
            case SHADOWFAX_PLAYED: {
                if (playSounds) {
                    Sounds.play(Sounds.Which.SHADOWFAX);
                }
                shadowfaxPlayed = true;
                shadowfaxMini.setCrossedOut(true);
                mainWindow.setMessage(Actor.Side.LIGHT, "Plays Shadowfax...");
                setWaitState(WaitState.MOVE);
                waitForMessage();
                break;

            }
            case BATTLE_MOVE: {
                if (!shadowfaxPlayed) {
                    mainWindow.setCenterMessageVisible(false);
                }

                final Move move = (Move) m.getPayload();
                logger.info("  The move is: " + move);
                animateAttack(move);

                if (checkBalrogInstakill(move)) {
                    break;
                }

                // Reveal enemy and animate
                String atkName = actorColl.get(move.attacker).name;
                mainWindow.setMessage(game.opponentSide,
                        "Attack by " + atkName + "!");
                showPortraitsForAttack(move);


                TimerTask tt = new TimerTask() {

                    @Override
                    public void run() {
                        battle(move);
                    }
                };
                Timer t = new Timer("BattleRoutineStarter");
                t.schedule(tt, 1250);

                break;
            }
            case PIPPIN_RETREAT_CHOICE: {
                // Where does Pippin retreat to?
                ETerritory retreatsTo = (ETerritory) m.getPayload();
                Move move = new Move(PIPPIN, NOBODY, defender.where, retreatsTo);

                logger.info("Got Pippin retreat move: " + move);

                doOpponentMoveAndAnimate(move);
                mainWindow.setMessage(LIGHT, "Pippin retreats to " + Territory.nameFor(retreatsTo) + ".");
                nextPlayer();

                break;
            }
            case GANDALF_WHITE_PLAYED: {
                if (playSounds) {
                    Sounds.play(Sounds.Which.GANDALF_WHITE);
                }
                dontPlayNextCling = true;
                gandalfWhiteMini.setCrossedOut(true);

                Actor gandalf = actorColl.get(GANDALF);
                gandalf.isAlive = true;
                Territory fangorn = terrColl.get(FANGORN);
                fangorn.addActor(gandalf);
                GamePiece gp = pieceColl.get(GANDALF);
                gp.setVisible(true);
                gp.setKnown(false);
                gp.setMovable(false);
                PieceOrderer.orderIn(terrColl, pieceColl, fangorn);
                mainWindow.setMessage(LIGHT, "Plays Gandalf the White.");
                if (enemyReport != null) {
                    enemyReport.update();
                }
                nextPlayer();

                break;
            }
            default:
                break;
        }

    }

    private Icon randomIcon(Side side) {
        EActor[] eas;
        if (side == LIGHT) {
            eas = Actor.LIGHT_ACTORS;
        } else {
            eas = Actor.DARK_ACTORS;
        }

        int i = (int) (Math.random() * eas.length);
        return Portraits.getIcon(eas[i]);
    }

    public void nextPlayer() {
        mainWindow.hidePortraits();

        if (shadowfaxPlayed) {
            thawPiecesAfterAnimation = false;
            shadowfaxPlayed = false;
            if (game.mySide == DARK) {
                mainWindow.setMessage(LIGHT, "Waiting for second move...");
            } else {
                mainWindow.setMessage(LIGHT, "You have one more move.");
            }

        } else {

            game.nextTurn();
            Battler.nextTurn();
            if (game.myTurn()) {

                if (!animator.isPlaying()) {
                    // No animation to wait for, thaw manually.
                    thawMySide();
                } else {
                    thawPiecesAfterAnimation = true;
                }

                if (playSounds && !dontPlayNextCling) {
                    Sounds.play(Sounds.Which.NOTIFICATION);
                }
                if (dontPlayNextCling) {
                    dontPlayNextCling = false;
                }

                if (showPopups) {
                    MessageBox.ok(mainWindow, "It's your turn!", "Your turn", randomIcon(game.mySide), true);
                }

            } else {
                freezeMySide();
                if (showCenteredMsg) {
                    mainWindow.setCenterMessage("Waiting for opponent to move...");
                    mainWindow.setCenterMessageVisible(true);
                }
            }

        }

        gameLoop();
    }

    private void showPortraitsForAttack(Move move) {
        EActor light, dark;
        int lightVal, darkVal;
        Side mySide = game.mySide;
        boolean myTurn = game.myTurn();

        if ((myTurn && mySide == DARK) || (!myTurn && mySide == LIGHT)) {
            light = move.defender;
            dark = move.attacker;
        } else {
            light = move.attacker;
            dark = move.defender;
        }

        Actor lightActor = actorColl.get(light);
        Actor darkActor = actorColl.get(dark);

        lightVal = lightActor.attackValue;
        darkVal = darkActor.attackValue;

        // Check Frodo + Sam boost special case.
        Territory lightWhere = terrColl.get(actorColl.get(light).where);
        if (light == EActor.SAM &&
                light == move.defender &&
                lightWhere.hasActor(EActor.FRODO) &&
                dark != WARG) {
            lightVal = 5;
        }

        Side attackingSide = (myTurn ? mySide : game.opponentSide);

        mainWindow.showPortraits(lightActor, darkActor, lightVal, darkVal, attackingSide);
    }

    private void showPortraitsForCurrentAttack() {
        showPortraitsForAttack(new Move(attacker.who, defender.who, NOWHERE, NOWHERE));
    }

    private boolean battlePippinSpecialCase(Move move) {

        logger.info("Checking Pippin");

        boolean pippinAttacks = (attacker.who == PIPPIN);
        boolean myTurn = game.myTurn();
        Territory to = terrColl.get(move.to);
        if (!pippinAttacks) {
            return false;
        }

        // Special case: Warg ignores text on piece.
        if (move.defender == WARG) {
            return false;
        }

        if (myTurn) {
            List<ETerritory> validRetreats = terrColl.getValidBackwardsRetreats(attacker, to.which);
            boolean canRetreat = validRetreats.size() > 0;
            // I attack with Pippin. Ask me about retreating. (If I can retreat.)
            boolean pippinRetreats = false;
            if (canRetreat) {
                int reply = MessageBox.yesNo(
                        mainWindow,
                        "Pippin faces " + defender.name + " in " + Territory.nameFor(to.which) + ".\n\nDo you want to retreat?",
                        "Pippin feat",
                        Portraits.getIcon(PIPPIN),
                        false);
                pippinRetreats = (reply == JOptionPane.YES_OPTION);
            }
            sendMessage(Message.newPippinRetreats(pippinRetreats));

            if (pippinRetreats) {
                savedMainPanel = mainWindow.getContainer();
                TerritorySelectionPanel tsp = new TerritorySelectionPanel(
                        savedMainPanel,
                        this,
                        validRetreats);
                mainWindow.setContainer(tsp);
                mainWindow.repaint();
            } else {
                // Dirty hack: if meeting Saruman, allow fallthrough in
                // battlePhaseTwo so that Saruman may decide whether we
                // should play cards. Return false to allow this fallthrough.
                if (move.defender == SARUMAN) {
                    return false;
                }

                pickSendAndWaitForCard();
            }

        } else {
            // Opponent attacks with Pippin. Wait for his decision.
            mainWindow.setMessage(LIGHT, "It's Pippin! Will he retreat?");
            setWaitState(WaitState.PIPPIN_DECISION);
            waitForMessage();
        }

        return true;
    }

    private enum SarumanCaseResult {

        NO_RESULT, PLAYS_CARDS, DOES_NOT_PLAY_CARDS, WAITING_FOR_DECISION
    }

    private SarumanCaseResult battleSarumanSpecialCase(Move move) {

        logger.info("Checking Saruman");

        Territory to = terrColl.get(move.to);

        boolean myTurn = game.myTurn();
        boolean playCards = true;

        boolean iAmSaruman = (attacker.who == SARUMAN && myTurn) ||
                (defender.who == SARUMAN && !myTurn);
        boolean oppIsSaruman = (attacker.who == SARUMAN & !myTurn) ||
                (defender.who == SARUMAN && myTurn);

        Actor opponent = (defender.who == SARUMAN ? attacker : defender);

        if (iAmSaruman) {
            int reply = MessageBox.yesNo(
                    mainWindow,
                    "Saruman faces " + opponent.name + " in " + Territory.nameFor(to.which) + ".\n\n" +
                    "Do you want to play cards?",
                    "Saruman feat",
                    Portraits.getIcon(SARUMAN),
                    false);

            playCards = (reply == JOptionPane.YES_OPTION);

            sendMessage(Message.newSarumanPlaysCards(playCards));

            if (!playCards) {
                return SarumanCaseResult.DOES_NOT_PLAY_CARDS;
            } else {
                return SarumanCaseResult.PLAYS_CARDS;
            }

        } else if (oppIsSaruman) {
            mainWindow.setMessage(DARK, "Waiting for Saruman's decision...");
            setWaitState(WaitState.SARUMAN_DECISION);
            waitForMessage();
            return SarumanCaseResult.WAITING_FOR_DECISION;
        }

        return SarumanCaseResult.NO_RESULT;
    }

    private boolean battleGandalfSpecialCase(Move move) {

        logger.info("Checking Gandalf");

        boolean myTurn = game.myTurn();
        boolean iAmGandalf = (myTurn && move.attacker == GANDALF) ||
                (!myTurn && move.defender == GANDALF);
        boolean oppIsGandalf = (!myTurn && move.attacker == GANDALF) ||
                (myTurn && move.defender == GANDALF);
        boolean iAmWarg = (myTurn && move.attacker == WARG) ||
                (!myTurn && move.defender == WARG);
        boolean oppIsWarg = (!myTurn && move.attacker == WARG) ||
                (myTurn && move.defender == WARG);

        if (iAmGandalf && !oppIsWarg) {
            assert game.mySide == LIGHT;
            mainWindow.setMessage(DARK, "Opponent picks card first...");
            setWaitState(WaitState.I_GANDALF_WAITING_FOR_CARD);
            waitForMessage();
            return true;
        }

        if (oppIsGandalf && !iAmWarg) {
            assert game.mySide == DARK;
            mainWindow.setMessage(LIGHT, "You have to pick your card first.");
            pickSendAndWaitForCard();
            return true;
        }
        return false;
    }

    private void battle(Move move) {

        attacker = actorColl.get(move.attacker);
        defender = actorColl.get(move.defender);

        assert attacker != null;
        assert defender != null;

        battler = new Battler(attacker, defender, terrColl);

        // When we are continuing a battle in the same region,
        // the attacker has no "where". But if it has,
        // we remove it so that retreats etc work better.
        if (attacker.where != NOWHERE) {
            terrColl.get(attacker.where).removeActor(attacker);
        }

        Result instaRes = battler.instantResolution();
        logger.info("Instant result = " + instaRes);

        if (instaRes != Result.NO_RESULT) {
            battleInstakill(attacker, defender, instaRes);
            return;
        }

        if (defender.isDark()) {
            battleDarkDefender(move);
        } else {
            battleLightDefender(move);
        }
    }

    private void battleDarkDefender(Move move) {
        if (battlePippinSpecialCase(move)) {
            // Pippin flees
            return;
        }

        SarumanCaseResult sr = battleSarumanSpecialCase(move);
        logger.info("SarumanCaseResult: " + sr);
        switch (sr) {
            case DOES_NOT_PLAY_CARDS:
                battlePhaseTwo(attacker, defender, attackerCard(), defenderCard());
                return;
            case PLAYS_CARDS:
                break;
            case WAITING_FOR_DECISION:
                return;
            default:
                break;
        }

        if (battleGandalfSpecialCase(move)) {
            logger.info("(was Gandalf special case)");
            // Handled.
            return;
        }

        logger.info("Fell through all cases, should pick card.");
        pickSendAndWaitForCard();
    }

    private void battleLightDefender(Move move) {

        Territory t = terrColl.get(defender.where);
        Actor sam = actorColl.get(SAM);
        if (t.hasActor(FRODO) && t.hasActor(SAM) && attacker.who != WARG) {
            sam.attackValue = 5;
        }

        // Frodo+Sam checks. We ignore the whole shebang if Warg's attacking,
        // then neither of the texts are any good.
        if ((defender.who == SAM || defender.who == FRODO) &&
                attacker.who != WARG) {

            boolean needOrcCheck = false;
            boolean needFrodoCheck = true;
            SamCaseResult r = battleFrodoSamSpecialCase(move);
            logger.info("SamCaseResult: " + r);
            switch (r) {
                case SAM_COVERS: {
                    defender = sam;
                    battler = new Battler(attacker, defender, terrColl);
                    showPortraitsForCurrentAttack();
                    needOrcCheck = true;
                    needFrodoCheck = false;
                    break;
                }
                case SAM_IS_ATTACKED_WITH_FRODO: {
                    needFrodoCheck = false;
                    needOrcCheck = true;
                    break;
                }
                case WAITING_FOR_DECISION: {
                    return;
                }
                default:
                    break;
            }

            if (needFrodoCheck) {
                FrodoCaseResult fr = battleFrodoSpecialCase(move);
                logger.info("FrodoCaseResult: " + fr);
                switch (fr) {
                    case FRODO_FLEES:
                        Battler.setOrcsHaveAttacked(true);
                        return;
                    case WAITING_FOR_DECISION:
                        return;
                    default:
                        needOrcCheck = true;
                        break;
                }
            }
            logger.info("Before orc check, can orcs instakill? " + Battler.orcsMayKill());
            if (needOrcCheck && attacker.who == ORCS && Battler.orcsMayKill()) {
                // We do actually have an instakill.
                battleInstakill(attacker, defender, Result.ATTACKER_WIN);
                return;
            }
        }

        SarumanCaseResult sr = battleSarumanSpecialCase(move);
        switch (sr) {
            case DOES_NOT_PLAY_CARDS:
                logger.info("DOES_NOT_PLAY_CARDS");
                battlePhaseTwo(attacker, defender, attackerCard(), defenderCard());
                return;
            case PLAYS_CARDS:
                logger.info("PLAYS_CARDS");
                break;
            case WAITING_FOR_DECISION:
                logger.info("WAITING_FOR_DECISION");
                return;
            default:
                logger.info("(default)");
                break;
        }

        if (battleGandalfSpecialCase(move)) {
            logger.info("(was Gandalf Special case");
            // Handled. Return.
            return;
        }

        pickSendAndWaitForCard();
    }

    private void battlePhaseTwo(final Actor attacker,
            Actor defender, ECard eAtkCard, ECard eDefCard) {

        battler.setAttackerCard(eAtkCard);
        battler.setDefenderCard(eDefCard);

        Result res = battler.resolve();
        logger.info("BATTLE: Resolving phase two. Result = " + res);

        BufferedImage attackerPortrait = Portraits.getLabeledNumberedPortrait(
                attacker.who, "Attacker", attacker.attackValue);
        BufferedImage defenderPortrait = Portraits.getLabeledNumberedPortrait(
                defender.who, "Defender", defender.attackValue);

        Deck atkDeck, defDeck;
        if (game.myTurn()) {
            atkDeck = myDeck;
            defDeck = (game.mySide == LIGHT ? new DarkDeck() : new LightDeck());
        } else {
            atkDeck = (game.mySide == LIGHT ? new DarkDeck() : new LightDeck());
            defDeck = myDeck;
        }

        Card attackerCard = atkDeck.getCard(eAtkCard);
        Card defenderCard = defDeck.getCard(eDefCard);

        switch (res) {
            case ATTACKER_WIN: {
                new BattleResultDialog(
                        mainWindow,
                        attackerCard,
                        defenderCard,
                        attackerPortrait,
                        defenderPortrait,
                        defender.name + " " + defender.isAre() + " defeated!",
                        BattleResultDialog.DEFAULT_TIMEOUT);

                final Territory defenderHome = terrColl.get(defender.where);

                Side winner = kill(defender);

                if (winner != null) {
                    triggerVictory(winner);
                    return;
                }

                ShelobVictoryReturnResult svrr = checkShelobVictoryReturn(attacker);
                final boolean shelobCase = handleShelobVictoryReturnResult(svrr, defenderHome);
                if (!shelobCase) {
                    battleCheckContinued(attacker, defenderHome);
                }
                break;
            }

            case DEFENDER_WIN: {
                new BattleResultDialog(
                        mainWindow,
                        attackerCard,
                        defenderCard,
                        attackerPortrait,
                        defenderPortrait,
                        attacker.name + " " + attacker.isAre() + " defeated!",
                        BattleResultDialog.DEFAULT_TIMEOUT);
                Side winner = kill(attacker);

                if (winner != null) {
                    triggerVictory(winner);
                    return;
                }

                ShelobVictoryReturnResult svrr = checkShelobVictoryReturn(defender);
                final boolean shelobCase = handleShelobVictoryReturnResult(svrr, null);
                if (!shelobCase) {
                    nextPlayer();
                }

                break;
            }

            case BOTH_DIE: {
                new BattleResultDialog(
                        mainWindow,
                        attackerCard,
                        defenderCard,
                        attackerPortrait,
                        defenderPortrait,
                        "Both characters are defeated!",
                        BattleResultDialog.DEFAULT_TIMEOUT);

                Side winnerA = kill(attacker);
                Side winnerB = kill(defender);
                if (winnerA == null && winnerB == null) {
                    PieceOrderer.orderAll(terrColl, pieceColl);
                    nextPlayer();

                } else {
                    assert winnerA == null || winnerB == null;
                    triggerVictory(winnerA == null ? winnerB : winnerA);
                }

                break;
            }

            case ATTACKER_RETREAT: {
                new BattleResultDialog(
                        mainWindow,
                        attackerCard,
                        defenderCard,
                        attackerPortrait,
                        defenderPortrait,
                        attacker.name + " " + Actor.retreats(attacker.who) + "!",
                        BattleResultDialog.DEFAULT_TIMEOUT);

                if (game.myTurn()) {
                    // I retreat.
                    // Bring up a selection panel and report my choice to my
                    // opponent. End my turn when done.
                    new RetreatHandler(mainWindow, attacker, attacker, defender.where,
                            terrColl, pieceColl, this, this, RetreatMode.END_TURN_WHEN_DONE);
                } else {
                    // Opponent retreats.
                    // Wait for a message telling where he retreats to.                    
                    setWaitState(WaitState.ATTACKER_RETREAT_MOVE);
                    waitForMessage();
                }

                break;
            }

            case DEFENDER_RETREAT: {
                new BattleResultDialog(
                        mainWindow,
                        attackerCard,
                        defenderCard,
                        attackerPortrait,
                        defenderPortrait,
                        defender.name + " " + Actor.retreats(defender.who) + "!",
                        BattleResultDialog.DEFAULT_TIMEOUT);

                if (game.myTurn()) {
                    // Opponent retreats.
                    // Wait for a message telling where he retreats to.
                    pieceColl.get(defender.who).setKnown(false);
                    setWaitState(WaitState.DEFENDER_RETREAT_MOVE);
                    waitForMessage();
                } else {
                    // I retreat.
                    // Bring up a selection panel and report my choice to my
                    // opponent. Do not end my turn. Check for continued battle.
                    new RetreatHandler(mainWindow, attacker, defender, defender.where,
                            terrColl, pieceColl, this, this,
                            RetreatMode.CHECK_CONTINUED_BATTLE_WHEN_DONE);
                }

                break;
            }

            case BOTH_RETREAT: {
                new BattleResultDialog(
                        mainWindow,
                        attackerCard,
                        defenderCard,
                        attackerPortrait,
                        defenderPortrait,
                        "Both retreat!",
                        BattleResultDialog.DEFAULT_TIMEOUT);

                Actor retreater = (game.myTurn() ? attacker : defender);
                retreatingFrom = defender.where;

                // Dark retreats first. Light waits for dark's move before doing their own.
                if (game.mySide == DARK) {
                    mainWindow.setMessage(DARK, "You retreat first.");
                    new RetreatHandler(mainWindow, attacker, retreater, retreatingFrom,
                            terrColl, pieceColl, this, this, RetreatMode.JUST_NOTIFY_WHEN_DONE);
                } else {
                    mainWindow.setMessage(DARK, "Waiting for retreat...");
                    setWaitState(WaitState.SIMULTANEOUS_RETREAT_MOVE);
                    waitForMessage();
                }

                break;
            }

            default: {
                throw new RuntimeException("Got erroneous result: " + res);
            }

        }

    }

    public void retreatPerformed(Move data) {
        // We get here during a both retreat situation, and we have selected our
        // retreat.
        // If we are light, this means that we act last and the turn is over.
        // If we are dark, this means that we should wait for light to retreat
        // before we end the turn.

        freezeMySide(); // Ugly hack. Don't know why we auto-thaw here really...

        if (game.mySide == DARK) {
            mainWindow.setMessage(LIGHT, "Waiting for retreat...");
            setWaitState(WaitState.SIMULTANEOUS_RETREAT_MOVE);
            waitForMessage();
        } else {
            nextPlayer();
        }
    }

    /**
     * 
     * @param actor
     * @return a winning side if this kill triggers a victory, null otherwise.
     */
    private Side kill(Actor actor) {
        if (actor.where != ETerritory.NOWHERE) {
            terrColl.get(actor.where).removeActor(actor);
        }

        pieceColl.get(actor.who).setVisible(false);
        actorViewer.nullifyIfCurrent(actor);

        actor.isAlive = false;

        if (enemyReport != null && actor.side == game.opponentSide) {
            enemyReport.update();
        }

        assert actor.where == NOWHERE;

        if (actor.who == FRODO) {
            return DARK;
        }

        // All Dark actors dead?
        if (actorColl.countLiving(DARK) == 0) {
            return LIGHT;
        }

        return null;
    }

    private void battleInstakill(final Actor attacker,
            Actor defender, Result instaRes) {
        BufferedImage attackerPortrait = Portraits.getLabeledNumberedPortrait(
                attacker.who, attacker.name, attacker.attackValue);

        // Hackfix: if Orcs instakill Sam with Frodo in the same region,
        // Sam's number should be 5. This is normally set in the
        // battleLightDefender method, but if we end up here we never
        // get a chance to do stuff there.

        int defVal = defender.attackValue;
        if (defender.who == SAM && terrColl.get(defender.where).hasActor(FRODO)) {
            defVal = 5;
        }
        BufferedImage defenderPortrait = Portraits.getLabeledNumberedPortrait(
                defender.who, defender.name, defVal);

        switch (instaRes) {
            case ATTACKER_WIN: {

                if (attacker.who == ORCS) {
                    Battler.setOrcsHaveAttacked(true);
                }

                new BattleResultDialog(
                        mainWindow,
                        null,
                        null,
                        attackerPortrait,
                        defenderPortrait,
                        defender.name + " " + defender.isAre() + " instantly defeated!",
                        BattleResultDialog.DEFAULT_TIMEOUT);
                Territory defenderHome = terrColl.get(defender.where);
                Side winner = kill(defender);
                if (winner == null) {
                    PieceOrderer.orderAll(terrColl, pieceColl);
                    mainWindow.hidePortraits();
                    battleCheckContinued(attacker, defenderHome);
                } else {
                    triggerVictory(winner);
                }

                break;
            }
            case DEFENDER_WIN: {
                new BattleResultDialog(
                        mainWindow,
                        null,
                        null,
                        attackerPortrait,
                        defenderPortrait,
                        attacker.name + " " + attacker.isAre() + " instantly defeated!",
                        BattleResultDialog.DEFAULT_TIMEOUT);
                Side winner = kill(attacker);
                if (winner == null) {
                    PieceOrderer.orderAll(terrColl, pieceColl);
                    nextPlayer();

                } else {
                    triggerVictory(winner);
                }

                break;
            }
            case BOTH_DIE: {
                new BattleResultDialog(
                        mainWindow,
                        null,
                        null,
                        attackerPortrait,
                        defenderPortrait,
                        "Both characters are instantly defeated!",
                        BattleResultDialog.DEFAULT_TIMEOUT);

                Side winnerA = kill(attacker);
                Side winnerB = kill(defender);
                if (winnerA == null && winnerB == null) {
                    PieceOrderer.orderAll(terrColl, pieceColl);
                    nextPlayer();

                } else {
                    assert winnerA == null || winnerB == null;
                    triggerVictory(winnerA == null ? winnerB : winnerA);
                }

                break;
            }
            default:
                break;
        }
    }

    private void battleContinued(Actor attacker, Actor defender, Territory inTerritory) {
        assert attacker != null;
        assert defender != null;
        assert inTerritory != null;
        assert attacker.who != NOBODY;
        assert defender.who != NOBODY;
        assert inTerritory.which != NOWHERE;
        assert inTerritory.getOccupantCount() > 0;

        Move move = new Move(attacker.who, defender.who, attacker.where, inTerritory.which);

        pieceColl.get(defender.who).mark();

        logger.info("Continued battle with move: " + move);

        myCard = null;
        opponentCard = null;

        if (game.myTurn()) {
            // I attacked. Simulate another move made.
            attackMoveMade(move);
        } else {
            // Opponent attacked. Wait for opponent to attack again.
            setWaitState(WaitState.MOVE);
            waitForMessage();
        }

    }

    private void battleContinued_pickRandomDefender(Actor attacker, Territory inTerritory) {
        assert inTerritory.getOccupantCount() > 0;

        int n = inTerritory.getOccupantCount();
        int i = (int) (Math.random() * n);
        defender = inTerritory.getOccupants().get(i);
        battleContinued(attacker, defender, inTerritory);
    }

    private void triggerVictory(Side whoWon) {
        pieceColl.setSideKnown(game.opponentSide, true);
        mainWindow.repaint();

        boolean iWon = whoWon == game.mySide;
        String sideStr = (game.mySide == LIGHT ? "Light" : "Dark");
        int points = actorColl.countLiving(whoWon);

        if (iWon) {
            game.myScore += points;
        } else {
            game.opponentScore += points;
        }

        mainWindow.setMyScore(game.myScore);
        mainWindow.setOpponentScore(game.opponentScore);
        mainWindow.updateInfoPanel();

        game.round++;
        boolean gameComplete = (game.round == 2);

        if (iWon) {
            if (playSounds) {
                Sounds.play(Sounds.Which.WON_ROUND);
            }

            JOptionPane.showMessageDialog(
                    mainWindow,
                    "You won this round by " + points +
                    (points == 1 ? " point." : " points."),
                    sideStr + " player wins",
                    JOptionPane.INFORMATION_MESSAGE);
        } else {
            if (playSounds) {
                Sounds.play(Sounds.Which.LOST_ROUND);
            }

            JOptionPane.showMessageDialog(
                    mainWindow,
                    "You lost this round,\ngiving your opponent " + points +
                    (points == 1 ? " point." : " points."),
                    sideStr + " player loses",
                    JOptionPane.INFORMATION_MESSAGE);
        }

        if (gameComplete) {
            boolean iWonTotal = game.myScore > game.opponentScore;
            boolean draw = game.myScore == game.opponentScore;

            String msg;

            if (iWonTotal) {
                if (playSounds) {
                    Sounds.play(Sounds.Which.WON_GAME);
                }

                msg = "You won over two rounds with\n" +
                        game.myScore + " points against " +
                        game.opponentScore + ".";
            } else if (!draw) {
                if (playSounds) {
                    Sounds.play(Sounds.Which.LOST_GAME);
                }

                msg = "You lost over two rounds with\n" +
                        game.myScore + " points against " +
                        game.opponentScore + ".";

            } else {
                msg = "It's a draw!\nBoth players got " + game.myScore + " points.";
            }

            JOptionPane.showMessageDialog(
                    mainWindow, msg, "Final result", JOptionPane.INFORMATION_MESSAGE);
            tearDown();

        } else {
            // Play new round as the opponent's side.
            newRound(game.opponentSide);
        }
    }

    public void shadowfaxPlayed() {
        shadowfaxPlayed = true;
    }

    public void appStateChange_popup(boolean showPopups) {
        this.showPopups = showPopups;
    }

    public void appStateChange_centerMsg(boolean showCenteredMsg) {
        this.showCenteredMsg = showCenteredMsg;
        mainWindow.setCenterMessageVisible(showCenteredMsg);
    }

    public void sendMessage(final Message m) {
        logger.info("Sending message: " + m);
        conn.sendMessage(m);
    }

    private void setupDecks() {
        if (game.mySide == LIGHT) {
            myDeck = new LightDeck();
            opponentDeck = new DarkDeck();
        } else {
            myDeck = new DarkDeck();
            opponentDeck = new LightDeck();
        }
        mainWindow.setMyDeck(myDeck);
        mainWindow.setOpponentDeck(opponentDeck);
    }

    private boolean checkDarkCantMoveVictory() {
        boolean isStuck = true;
        for (Actor a : actorColl) {
            if (a.isAlive && a.side == DARK) {
                if (a.who == F_NAZGUL) {
                    // They can always fly out.
                    return false;
                }
                isStuck &= a.where == SHIRE;
            }
        }
        if (isStuck && darkHasPlayedRecall) {
            // Dark is stuck in The Shire. Instalose.
            triggerVictory(LIGHT);
            return true;
        }
        return false;
    }
}
